#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Purpose: Network layer for thuban
# Created: 11/19/2008
# Copyright (c) 2008,2009 by University College London
# Authors:
# Didrik Pinte <dpinte@dipole-consulting.com>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with HydroPlatform for details.
"""
Network classes definition (project, network, nodes, links, etc.)
"""

import logging
import networkx
import sqlalchemy
from sqlalchemy import and_

import shapely.geometry

import Thuban
from Thuban.Model.proj import Projection
from Thuban.Lib.connector import Publisher


NETWORK_NODE_ADDED = "network_node_added"
NETWORK_LINK_ADDED = "network_link_added"
PROJECT_NODETYPE_ADDED = "project_nodetype_added"
PROJECT_NODETYPE_REMOVED = "project_nodetype_removed"

class Project(object):
    """
    Base class for to manage networks : a Project is a container for a list 
    of networks with some specific time information, units and projection
    """
    
    def __init__(self, name, description, projection=None):
        self.name = name
        self.description = description
        self.projection = None
        if projection is not None:
            assert isinstance(projection, Thuban.Model.proj.Projection)
            self.SetProjection(projection)
        else:
            self.projection_params = []
    
    def __repr__(self):
        return "<Project %s/id %s>" % (self.name, self.id)
   
    def GetProjection(self):
        """
        Returns a Thuban.Model.proj.Projection object or None if 
        not initiliazed
        """
        if not hasattr(self, 'projection') or self.projection is None:
            if not hasattr(self, 'projection_params') or \
               len(self.projection_params) == 0:
                return None
            self.projection = Projection(self.projection_params, 
                                         self.projection_name, 
                                         self.projection_epsg)
        return self.projection
    
    def SetProjection(self, projection):
        '''
        Receives a Thuban.Model.proj.Projection object. 
        '''
        try:
            self.projection = projection
            if projection.GetAllParameters() is None:
                self.projection_params = []
            else :
                self.projection_params = projection.GetAllParameters()
            self.projection_name = projection.GetName()
            self.projection_epsg = projection.EPSGCode()
        except Exception, e:
            logging.error("Problem while setting projection for network. Projection was : %s" % projection)
            logging.exception(e)
            self.projection = None
        
class Network(object, Publisher):
    """
    The Network class publishes two events : NETWORK_LINK_ADDED and 
    NETWORK_NODE_ADDED. 
    """
    
    def __init__(self, name, project):
        self.name = name
        self.project_id = project.id
    
    def load_graph(self):
        """
        Load a networkx DiGraph based on the list of nodes and links in 
        the network
        
        FIXME : this method could be renamed as "private"
        """
        self.graph = networkx.DiGraph()
        for node in self.nodes:
            self.graph.add_node(node)
        for link in self.links:
            self.graph.add_edge(link.start, link.end)
  
    def has_link(self, node1, node2):
        """
        Test if the network has a link between node1 and node2
        """
        if not hasattr(self, 'graph'):
            self.load_graph()
        try:
            return self.graph.has_edge(node1, node2)
        except TypeError, exc:
            logging.exception(exc)
            logging.error('Error in has_link. Problem with %s and %s' \
                          % (node1, node2))
            return False
        
    def __repr__(self):
        return "<Network %s linked to project %s" % (self.name, self.project_id)
    
    
    def add_node(self, node):
        """
        Add a node to the network and issue a NETWORK_NODE_ADDED message
        """        
        self.nodes.append(node)
        self.issue(NETWORK_NODE_ADDED, node)
        if hasattr(self, 'graph'):
            self.graph.add_node(node)
        
    def add_link(self, link):
        """
        Add a link to the network and issue NETWORK_LINK_ADDED
        """
        self.links.append(link)
        self.issue(NETWORK_LINK_ADDED, link)
        if hasattr(self, 'graph'):
            self.graph.add_edge(link.start, link.end)
            
    def remove_link(self, link):
        """
        Remove link from the network 
        """
        if link in self.links:
            self.links.remove(link)
            try:
                if hasattr(self, 'graph'):
                    self.graph.remove_edge(link.start, link.end)
            except networkx.NetworkXError, e:
                # error fired if the user has already remove the node
                logging.exception(e)
            
    def remove_node(self, node):
        """
        Remove the node from the network
        """
        if node in self.nodes:
            self.nodes.remove(node)
            if hasattr(self, 'graph'):
                self.graph.remove_node(node)

class NodeType(object):
    """
    Node type class describes a type of node in the network by 
    some styling information and name/description
    """
    
    def __init__(self, name, description, project, 
                 shape=u"triangle", pen=u"black", brush=u"black", size=10, \
                 visible_on_map=True):
        self.name = name
        self.description = description
        self.project_id = project.id
        self.shape = shape
        self.pen = pen
        self.brush = brush
        self.size = size
        self.visible_on_map = visible_on_map
   
    def __repr__(self):
        return "<NoteType %s>" % self.name
        
    
    def copy_to_project(self, project):
        """
        Copy this node to the given project
        
        FIXME : to be consistant this method should also copy the fields and 
        units associates to this node type
        """
        nt = NodeType(self.name, 
                      self.description, 
                      project, 
                      self.shape, self.pen, self.brush, 
                      self.size)
        
        return nt
        
class NodeField(object):
    """
    Field associate to a NodeType
    
    The attribute_type attribute is a Field trait object that will
    be pickled to the database ...
    """
    
    def __init__(self, node_type_id, attribute_type, attribute_name, group=None):
        self.node_type_id = node_type_id
        self.attribute_type = attribute_type
        self.name = attribute_name
        if group is not None:
            self.group_id = group.id
        
    
    def __repr__(self):
        return "<Field %s for node type %s>" % (self.name, self.type.name)
        
    def copy_to_otype(self, nodetype):
        """
        Copy to another object type
        
        FIXME : this method should check if the units associates with the 
        attribute_type object does exist in the target project
        """ 
        field = NodeField(nodetype.id, 
                          self.attribute_type, 
                          self.name, 
                          self.group)
        return field
    
class FieldGroup(object):
    """
    Classes describing a group of fields
    """
    
    def __init__(self, name):
        self.name = name
        
    def __repr__(self):
        return "<Field group %s>" % self.name
    
    def __str__(self):
        return "Group %s (id %i)" % (self.name, self.id)

class FieldValue(object):
    '''
    This is the base class for the NodeValue and LinkValue that is 
    used in model/fields.py as a reference
    '''
    def copy_to_object(self, obj):
        """
        Copy the field value from this object to the given object
        """
        value = self.__class__(obj.id, 
                          self.attribute_id, 
                          self.value)
        return value   
    
      
class NodeValue(FieldValue):
    """
    Value associate to a node for a given field
    """
    def __init__(self, node_id, field_id, value):
        self.node_id = node_id
        self.attribute_id = field_id
        self.value = value
   
    def __repr__(self):
        return "<Node %s - value for attribute %s is %s" \
               % (self.node_id, self.attribute_id, self.value)

class NetworkObjectMixin(object):
    
    def isSelected(self):
        if not hasattr(self, 'selected'):
            self.selected = False
        return self.selected

    def getFieldValue(self, session, field):
        '''
        Return an existing value for the given field if any,
        Otherwise returns None
        '''        
        if isinstance(self, Node):
            ClsValue = NodeValue
            oid = NodeValue.node_id
        elif isinstance(self, Link):
            ClsValue = LinkValue
            oid = LinkValue.link_id
        return session.query(ClsValue).filter( \
            and_(oid==self.id,
                 ClsValue.attribute_id == field.id)).first()
        
            
    def getName(self):
        return self.name    
    
class Node(NetworkObjectMixin):
    
    def __init__(self, name, project, node_type, notes=None):
        self.name = name
        self.project_id = project.id
        self.type_id = node_type.id
        if notes is not None: self.notes = notes
        
    def __repr__(self):
        return "<Node %s>" % self.id

    def getNewValueForField(self, field):
        '''
        Return a new NodeValue for the given NodeField
        '''
        return NodeValue(self.id, field.id, None)
    
    def getGeometry(self):
        if hasattr(self, "geom"):
            return self.geom
        if self.x is not None and self.y is not None:
            self.geom = shapely.geometry.Point(self.x, self.y)
            return self.geom
        else: return None
        
    
    def set_coord(self, x, y, dbsession=None):
        '''
        This method will set the coordinates and refresh the geometry
        '''
        self.x = x
        self.y = y
        if hasattr(self, 'geom'):
            del(self.geom)
            
        if dbsession is not None:
            links = dbsession.query(Link).filter(\
                sqlalchemy.or_(Link.start_node_id==self.id,
                               Link.end_node_id==self.id)).all()
            for link in links:
                if hasattr(link, "geom"):
                    del link.geom
                    
    def copy_to_project(self, project):
        """
        Copy this node to project
        """
        for ntype in project.nodetypes:
            if ntype.name == self.type.name:
                new_node = Node(self.name, project, ntype, self.notes) 
                new_node.set_coord(self.x, self.y)
                return new_node
        else:
            # did not found a corresponding node type --> should raise an error
            raise Exception("No matching node type in the given project")
        
        
        
        
class Link(NetworkObjectMixin):
    
    def __init__(self, project, start_node, end_node, link_type):
        self.project_id = project.id
        self.start_node_id = start_node.id
        self.end_node_id = end_node.id
        self.type_id = link_type.id
        self.name = u"%s_%s" % (start_node.name, end_node.name)
    
    def __repr__(self):
        return "Link %s [%s --> %s] belongs to %s" \
                   % (self.id, self.start_node_id, self.end_node_id, \
                      self.project_id)

    def getNewValueForField(self, field):
        '''
        Return a new LinkValue for the given LinkField
        '''
        return LinkValue(self.id, field.id, None) 
    
    def getGeometry(self, bidirectionnal=False):
        if hasattr(self, "geom"):
            return self.geom
        if self.start.x is not None and self.start.y is not None \
           and self.end.x is not None and self.end.y is not None:
            if bidirectionnal is True:
                (x3,y3) = get_thirdpoint(self.start, self.end)
                points = [[self.start.x, self.start.y],
                    [x3,y3],
                    [self.end.x, self.end.y]]
            else:
                points = [[self.start.x, self.start.y],
                         [self.end.x, self.end.y]]
            self.geom = shapely.geometry.LineString(points)
            return self.geom
        else: return None
        
        
    def copy_to_project(self, project):
        """
        Copy this node to project
        """
        start = end = None
        for ltype in project.linktypes:
            if ltype.name == self.type.name:
                for node in project.nodes:
                    if node.name == self.start.name:
                        start = node
                    if node.name == self.end.name:
                        end = node
                    if start is not None and end is not None:
                        new_link = Link(project, start, end, ltype)
                        new_link.name = self.name
                        return new_link
                else:
                    raise Exception("No matching node for link in the given project")
        else:
            # did not found a corresponding node type --> should raise an error
            raise Exception("No matching link type in the given project")                
        
class LinkType(object):
    
    def __init__(self, name, description, project, 
                  pen=u"black", style=u"wx.SOLID", size=1):
        self.name = name
        self.description = description
        self.project_id = project.id
        self.size = size
        self.pen = pen
        self.style = style
   
    def __repr__(self):
        return "<LinkType %s>" % self.name
    
    def copy_to_project(self, project):
        lt = LinkType(self.name, 
                      self.description, 
                      project,
                      self.pen,
                      self.style,
                      self.size)
        return lt
        
class LinkField(object):
    
    def __init__(self, link_type_id, attribute_type, attribute_name, group):
        self.link_type_id = link_type_id
        self.attribute_type = attribute_type
        self.name = attribute_name
        if group is not None:
            self.group_id = group.id
    
    def __repr__(self):
        return "<Field %s for link type %s>" % (self.name, self.type.name)
    
    def copy_to_otype(self, linktype):
        """
        Copy to another object type
        """
        field = LinkField(linktype.id, 
                          self.attribute_type, 
                          self.name, 
                          self.group)
        return field    
        
class LinkValue(FieldValue):
    
    def __init__(self, link_id, field_id, value):
        self.link_id = link_id
        self.attribute_id = field_id
        self.value = value
   
    def __repr__(self):
        return "<Link %s - value for attribute %s is %s" \
               % (self.link_id, self.attribute_id, self.value)
    
    
def get_thirdpoint(p1, p2):
    '''
    Compute mid point on a link
    '''
    
    if p1.x < p2.x:
        lx = (p2.x - p1.x)
        if p1.y < p2.y:
            ly = (p2.y - p1.y)
            x = p1.x  + lx/2.0 + lx/10.0
            y = p1.y + ly/2.0 - ly/10.0
        else :
            ly = (p1.y - p2.y)
            x = p1.x  + lx/2.0 - lx/10.0
            y = p1.y - ly/2.0 - ly/10.0
    else:
        lx = (p1.x - p2.x)
        if p1.y < p2.y:
            ly = (p2.y - p1.y)
            x = p2.x  + lx/2.0  + lx/10.0
            y = p1.y + ly/2.0 + ly/10.0
        else :
            ly = (p1.y - p2.y)
            x = p2.x  + lx/2.0  - ly/10.0
            y = p1.y - ly/2.0 + ly/10.0        
            
    return (x, y)
